page.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { notFound } from 'next/navigation';
  2. import Image from 'next/image';
  3. import { fetchJson } from '@/lib/utils/server';
  4. import type { GameDetailResponse } from '@/types/response/store/game';
  5. import GameMediaGallery from './GameMediaGallery';
  6. import GameProductCarousel from './GameProductCarousel';
  7. export async function generateMetadata({ params }: { params: Promise<{ id: string }> })
  8. {
  9. const { id } = await params;
  10. const res = await fetchJson<GameDetailResponse>(`/api/store/games/${id}`, { method: 'GET' });
  11. return { title: res.success && res.data ? res.data.korName : '게임' };
  12. }
  13. export default async function GameDetailPage({ params }: { params: Promise<{ id: string }> })
  14. {
  15. const { id } = await params;
  16. const res = await fetchJson<GameDetailResponse>(`/api/store/games/${id}`, { method: 'GET' });
  17. if (!res.success || !res.data) {
  18. notFound();
  19. }
  20. const game = res.data;
  21. return (
  22. <div className="mx-auto w-full max-w-screen-xl px-4 py-6">
  23. <header className="mb-5">
  24. <h1 className="text-2xl font-bold">{game.korName}</h1>
  25. {game.engName && <p className="text-sm text-[var(--text-secondary)]">{game.engName}</p>}
  26. </header>
  27. <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
  28. {/* 좌측: 미디어 갤러리 + 상품 + 설명 */}
  29. <div className="flex min-w-0 flex-col gap-6 lg:col-span-2">
  30. <GameMediaGallery key={game.id} trailerUrl={game.trailerUrl} images={game.images} />
  31. <GameProductCarousel products={game.randomProducts} />
  32. {game.description && (
  33. <section>
  34. <h2 className="mb-3 text-lg font-bold">게임 소개</h2>
  35. <div className="max-w-none text-sm leading-relaxed [&_a]:text-[var(--text-link)] [&_a]:underline [&_h1]:mb-1 [&_h1]:mt-3 [&_h1]:text-lg [&_h1]:font-bold [&_h2]:mb-1 [&_h2]:mt-3 [&_h2]:font-bold [&_img]:my-2 [&_img]:rounded [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5" dangerouslySetInnerHTML={{ __html: game.description }} />
  36. </section>
  37. )}
  38. </div>
  39. {/* 우측: 정보 사이드바 */}
  40. <aside className="flex flex-col gap-4">
  41. {(game.bigImage || game.thumbnail) && (
  42. <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-[var(--bg-elevated)]">
  43. <Image src={(game.bigImage ?? game.thumbnail)!} alt={game.korName} fill unoptimized sizes="(max-width: 1024px) 100vw, 33vw" className="object-cover" />
  44. </div>
  45. )}
  46. <dl className="flex flex-col gap-3 rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4 text-sm">
  47. <div className="flex justify-between gap-3">
  48. <dt className="text-[var(--text-secondary)]">퍼블리셔</dt>
  49. <dd className="text-right font-medium">{game.publisher}</dd>
  50. </div>
  51. {game.releaseDate && (
  52. <div className="flex justify-between gap-3">
  53. <dt className="text-[var(--text-secondary)]">출시일</dt>
  54. <dd className="text-right font-medium">{game.releaseDate.split('-').join('.')}</dd>
  55. </div>
  56. )}
  57. {game.genres.length > 0 && (
  58. <div className="flex flex-col gap-1">
  59. <dt className="text-[var(--text-secondary)]">장르</dt>
  60. <dd className="flex flex-wrap gap-1.5">
  61. {game.genres.map(g => (
  62. <span key={g} className="rounded-full border border-[var(--border-default)] px-2 py-0.5 text-xs">{g}</span>
  63. ))}
  64. </dd>
  65. </div>
  66. )}
  67. {game.categories.length > 0 && (
  68. <div className="flex flex-col gap-1">
  69. <dt className="text-[var(--text-secondary)]">카테고리</dt>
  70. <dd className="flex flex-wrap gap-1.5">
  71. {game.categories.map(c => (
  72. <span key={c} className="rounded-full border border-[var(--border-default)] px-2 py-0.5 text-xs">{c}</span>
  73. ))}
  74. </dd>
  75. </div>
  76. )}
  77. </dl>
  78. {game.languages.length > 0 && (
  79. <div className="rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4">
  80. <h2 className="mb-2 text-sm font-bold">지원 언어</h2>
  81. <table className="w-full text-sm">
  82. <thead>
  83. <tr className="text-xs text-[var(--text-secondary)]">
  84. <th className="py-1 text-left font-medium">언어</th>
  85. <th className="py-1 text-center font-medium">인터페이스</th>
  86. <th className="py-1 text-center font-medium">음성</th>
  87. <th className="py-1 text-center font-medium">자막</th>
  88. </tr>
  89. </thead>
  90. <tbody>
  91. {game.languages.map(l => (
  92. <tr key={l.korName} className="border-t border-[var(--border-default)]">
  93. <td className="py-1.5">{l.korName}</td>
  94. <td className="py-1.5 text-center text-[var(--text-link)]">{l.interface ? '✓' : ''}</td>
  95. <td className="py-1.5 text-center text-[var(--text-link)]">{l.fullAudio ? '✓' : ''}</td>
  96. <td className="py-1.5 text-center text-[var(--text-link)]">{l.subtitles ? '✓' : ''}</td>
  97. </tr>
  98. ))}
  99. </tbody>
  100. </table>
  101. </div>
  102. )}
  103. {(game.minSpec || game.recommendedSpec) && (
  104. <div className="flex flex-col gap-3 rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4 text-sm">
  105. <h2 className="font-bold">시스템 사양</h2>
  106. {game.minSpec && (
  107. <div>
  108. <p className="mb-1 text-[var(--text-secondary)]">최소 사양</p>
  109. <p className="whitespace-pre-line">{game.minSpec}</p>
  110. </div>
  111. )}
  112. {game.recommendedSpec && (
  113. <div>
  114. <p className="mb-1 text-[var(--text-secondary)]">권장 사양</p>
  115. <p className="whitespace-pre-line">{game.recommendedSpec}</p>
  116. </div>
  117. )}
  118. </div>
  119. )}
  120. {game.link && (
  121. <a
  122. href={game.link}
  123. target="_blank"
  124. rel="noopener noreferrer"
  125. className="rounded-lg bg-[var(--text-link)] px-4 py-2.5 text-center text-sm font-semibold text-white"
  126. >
  127. 공식 사이트 방문
  128. </a>
  129. )}
  130. </aside>
  131. </div>
  132. </div>
  133. );
  134. }